﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;

namespace Windows8Accelerator.Core.Xaml.Serialization
{
    public static class BinarySerializer
    {
        private static Dictionary<Type, List<PropertyInfo>> TypeProperties = new Dictionary<Type, List<PropertyInfo>>();
        private static Dictionary<Type, List<FieldInfo>> TypeFields = new Dictionary<Type, List<FieldInfo>>();

        public static void Serialize(Stream stream, object obj)
        {
            var types = new List<Type>();
            var objects = new List<object>();

            var headerPosition = stream.Position;
            Write(stream, headerPosition);

            WriteObject(stream, obj, types, objects);

            headerPosition = stream.Position;
            
            WriteTypes(stream, types);
            
            stream.Position = 0;
            Write(stream, headerPosition);

            stream.Flush();
        }

        private static void Write(Stream stream, bool value)
        {
            var bytes = BitConverter.GetBytes(value);
            stream.Write(bytes, 0, bytes.Length);
        }
        private static void Write(Stream stream, char value)
        {
            var bytes = BitConverter.GetBytes(value);
            stream.Write(bytes, 0, bytes.Length);
        }
        private static void Write(Stream stream, byte value)
        {
            stream.WriteByte(value);
        }
        private static void Write(Stream stream, double value)
        {
            var bytes = BitConverter.GetBytes(value);
            stream.Write(bytes, 0, bytes.Length);
        }
        private static void Write(Stream stream, decimal value)
        {
            var bits = decimal.GetBits(value);
            Write(stream, bits[0]);
            Write(stream, bits[1]);
            Write(stream, bits[2]);
            Write(stream, bits[3]);
        }
        private static void Write(Stream stream, float value)
        {
            var bytes = BitConverter.GetBytes(value);
            stream.Write(bytes, 0, bytes.Length);
        }
        private static void Write(Stream stream, int value)
        {
            var bytes = BitConverter.GetBytes(value);
            stream.Write(bytes, 0, bytes.Length);
        }
        private static void Write(Stream stream, long value)
        {
            var bytes = BitConverter.GetBytes(value);
            stream.Write(bytes, 0, bytes.Length);
        }
        private static void Write(Stream stream, short value)
        {
            var bytes = BitConverter.GetBytes(value);
            stream.Write(bytes, 0, bytes.Length);
        }
        private static void Write(Stream stream, uint value)
        {
            var bytes = BitConverter.GetBytes(value);
            stream.Write(bytes, 0, bytes.Length);
        }
        private static void Write(Stream stream, ulong value)
        {
            var bytes = BitConverter.GetBytes(value);
            stream.Write(bytes, 0, bytes.Length);
        }
        private static void Write(Stream stream, ushort value)
        {
            var bytes = BitConverter.GetBytes(value);
            stream.Write(bytes, 0, bytes.Length);
        }
        private static void Write(Stream stream, Guid guid)
        {
            var bytes = guid.ToByteArray();
            stream.Write(bytes, 0, bytes.Length);
        }
        private static void Write(Stream stream, TimeSpan timeSpan)
        {
            Write(stream, timeSpan.Ticks);
        }
        private static void Write(Stream stream, DateTime dateTime)
        {
            Write(stream, dateTime.Ticks);
        }
        private static void Write(Stream stream, string s)
        {
            var bytes = UnicodeEncoding.UTF8.GetBytes(s);
            Write(stream, bytes.Length);
            stream.Write(bytes, 0, bytes.Length);
        }

        private static Guid ReadGuid(Stream stream)
        {
            byte[] buffer = new byte[16];
            stream.Read(buffer, 0, buffer.Length);
            return new Guid(buffer);
        }
        private static TimeSpan ReadTimeSpan(Stream stream)
        {
            return new TimeSpan(ReadInt64(stream));
        }
        private static DateTime ReadDateTime(Stream stream)
        {
            return new DateTime(ReadInt64(stream));
        }
        private static string ReadString(Stream stream)
        {
            var length = ReadInt32(stream);
            byte[] buffer = new byte[length];
            stream.Read(buffer, 0, buffer.Length);
            return UnicodeEncoding.UTF8.GetString(buffer,0,buffer.Length);
        }
        private static bool ReadBool(Stream stream)
        {
            byte[] buffer = new byte[sizeof(bool)];
            stream.Read(buffer, 0, buffer.Length);
            return BitConverter.ToBoolean(buffer, 0);
        }
        private static byte ReadByte(Stream stream)
        {
            byte[] buffer = new byte[1];
            stream.Read(buffer, 0, buffer.Length);
            return buffer[0];
        }
        private static char ReadChar(Stream stream)
        {
            byte[] buffer = new byte[sizeof(char)];
            stream.Read(buffer, 0, buffer.Length);
            return BitConverter.ToChar(buffer, 0);
        }
        private static double ReadDouble(Stream stream)
        {
            byte[] buffer = new byte[sizeof(double)];
            stream.Read(buffer, 0, buffer.Length);
            return BitConverter.ToDouble(buffer, 0);
        }
        private static decimal ReadDecimal(Stream stream)
        {
            int[] bits = new int[4];
            bits[0] = ReadInt32(stream);
            bits[1] = ReadInt32(stream);
            bits[2] = ReadInt32(stream);
            bits[3] = ReadInt32(stream);
            return new decimal(bits);
        }
        private static short ReadInt16(Stream stream)
        {
            byte[] buffer = new byte[sizeof(short)];
            stream.Read(buffer, 0, buffer.Length);
            return BitConverter.ToInt16(buffer, 0);
        }
        private static int ReadInt32(Stream stream)
        {
            byte[] buffer = new byte[sizeof(int)];
            stream.Read(buffer, 0, buffer.Length);
            return BitConverter.ToInt32(buffer, 0);
        }
        private static long ReadInt64(Stream stream)
        {
            byte[] buffer = new byte[sizeof(long)];
            stream.Read(buffer, 0, buffer.Length);
            return BitConverter.ToInt64(buffer, 0);
        }
        private static float ReadSingle(Stream stream)
        {
            byte[] buffer = new byte[sizeof(float)];
            stream.Read(buffer, 0, buffer.Length);
            return BitConverter.ToSingle(buffer, 0);
        }
        private static ushort ReadUInt16(Stream stream)
        {
            byte[] buffer = new byte[sizeof(ushort)];
            stream.Read(buffer, 0, buffer.Length);
            return BitConverter.ToUInt16(buffer, 0);
        }
        private static uint ReadUInt32(Stream stream)
        {
            byte[] buffer = new byte[sizeof(uint)];
            stream.Read(buffer, 0, buffer.Length);
            return BitConverter.ToUInt32(buffer, 0);
        }
        private static ulong ReadUInt64(Stream stream)
        {
            byte[] buffer = new byte[sizeof(ulong)];
            stream.Read(buffer, 0, buffer.Length);
            return BitConverter.ToUInt64(buffer, 0);
        }


        private static void WriteTypes(Stream stream, List<Type> types)
        {
            Write(stream, types.Count);
            foreach (var type in types)
            {
                WriteType(stream, type);
            }
        }

        private static void WriteType(Stream stream, Type type)
        {
            Write(stream, type.AssemblyQualifiedName);

            if (TypeFields.ContainsKey(type))
            {
                Write(stream, TypeFields[type].Count);
                foreach (var member in TypeFields[type])
                {
                    Write(stream, member.Name);
                }
            }
            else
            {
                Write(stream, 0);
            }

            if (TypeProperties.ContainsKey(type))
            {
                Write(stream, TypeProperties[type].Count);
                foreach (var member in TypeProperties[type])
                {
                    Write(stream, member.Name);
                }
            }
            else
            {
                Write(stream, 0);
            }
        }

        private static void WriteObject(Stream stream, object obj, List<Type> types, List<object> objects)
        {
            if (obj == null)
            {
                Write(stream, -1);
            }
            else
            {
                var type = obj.GetType();
                if (!types.Contains(type))
                    types.Add(type);
                Write(stream, types.IndexOf(type));

                if (!WritePrimitiveValue(stream, obj, type))
                {
                    var typeInfo = type;
                    if (typeInfo.IsEnum)
                    {
                        var enumBaseType = Enum.GetUnderlyingType(type);
                        WritePrimitiveValue(stream, obj, enumBaseType);
                    }
                    else 
                    {
                        var indexof = objects.IndexOf(obj);
                        if (indexof != -1)
                        {
                            Write(stream, true);
                            Write(stream, indexof);
                        }
                        else
                        {
                            Write(stream, false);

                            objects.Add(obj);
                            indexof = objects.IndexOf(obj);
                            Write(stream, indexof);

                            if (typeInfo.IsArray)
                            {
                                WriteArray(stream, (Array)obj, types, objects);
                            }
                            else if (typeInfo.GetInterfaces().Contains(typeof(IDictionary)))
                            {
                                WriteDictionary(stream, (IDictionary)obj, types, objects);
                            }
                            else if (typeInfo.GetInterfaces().Contains(typeof(IList)))
                            {
                                WriteList(stream, (IList)obj, types, objects);
                            }
                            else
                            {
                                WriteProperties(stream, obj, types, objects);
                            }
                        }
                    }
                }
            }
        }

        private static void WriteList(Stream stream, IList obj, List<Type> types, List<object> objects)
        {
            Write(stream, obj.Count);
            foreach (var item in obj)
            {
                var value = item;
                WriteObject(stream, value, types, objects);
            }
        }

        private static void WriteDictionary(Stream stream, IDictionary obj, List<Type> types, List<object> objects)
        {
            Write(stream, obj.Count);
            foreach (DictionaryEntry item in (IDictionary)obj)
            {
                WriteObject(stream, item.Key, types, objects);
                WriteObject(stream, item.Value, types, objects);
            }
        }

        private static void WriteArray(Stream stream, Array obj, List<Type> types, List<object> objects)
        {
            //TODO: suportar N dimensoes
            if (obj.Rank != 1)
                throw new NotImplementedException();

            Write(stream, obj.Rank);

            Write(stream, obj.GetLowerBound(0));
            Write(stream, obj.GetUpperBound(0));

            for (int i = obj.GetLowerBound(0); i < obj.GetUpperBound(0); i++)
            {
                var value = obj.GetValue(i);
                WriteObject(stream, value, types, objects);
            }
        }

        private static bool WritePrimitiveValue(Stream stream, object obj, Type type)
        {
            if (type == typeof(bool))
            {
                Write(stream, (bool)obj);
                return true;
            }
            else if (type == typeof(char))
            {
                Write(stream, (char)obj);
                return true;
            }
            else if (type == typeof(byte))
            {
                Write(stream, (byte)obj);
                return true;
            }
            else if (type == typeof(decimal))
            {
                Write(stream, (decimal)obj);
                return true;
            }
            else if (type == typeof(double))
            {
                Write(stream, (double)obj);
                return true;
            }
            else if (type == typeof(float))
            {
                Write(stream, (float)obj);
                return true;
            }
            else if (type == typeof(short))
            {
                Write(stream, (short)obj);
                return true;
            }
            else if (type == typeof(int))
            {
                Write(stream, (int)obj);
                return true;
            }
            else if (type == typeof(long))
            {
                Write(stream, (long)obj);
                return true;
            }
            else if (type == typeof(string))
            {
                Write(stream, (string)obj);
                return true;
            }
            else if (type == typeof(DateTime))
            {
                Write(stream, (DateTime)obj);
                return true;
            }
            else if (type == typeof(TimeSpan))
            {
                Write(stream, (TimeSpan)obj);
                return true;
            }
            else if (type == typeof(Guid))
            {
                Write(stream, (Guid)obj);
                return true;
            }
            else if (type == typeof(ushort))
            {
                Write(stream, (ushort)obj);
                return true;
            }
            else if (type == typeof(uint))
            {
                Write(stream, (uint)obj);
                return true;
            }
            else if (type == typeof(ulong))
            {
                Write(stream, (ulong)obj);
                return true;
            }
            else if (obj is Type)
            {
                Write(stream, ((Type)obj).AssemblyQualifiedName);
                return true;
            }
            return false;
        }


        private static void WriteProperties(Stream stream, object obj, List<Type> types, List<object> objects)
        {
            var fields = GetFields(obj.GetType());
            foreach (var member in fields)
            {
                var value = GetValue(member, obj);
                WriteObject(stream, value, types, objects);
            }

            var properties = GetProperties(obj.GetType());
            foreach (var member in properties)
            {
                var value = GetValue(member, obj);
                WriteObject(stream, value, types, objects);
            }
        }

        private static object GetValue(PropertyInfo property, object obj)
        {
            try
            {
                return property.GetValue(obj, null);
            }
            catch { }
            return null;
        }

        private static object GetValue(FieldInfo field, object obj)
        {
            try
            {
                return field.GetValue(obj);
            }
            catch { }
            return null;
        }

        private static List<PropertyInfo> GetProperties(Type type)
        {
            if (!TypeProperties.ContainsKey(type))
            {
                var members = new List<PropertyInfo>();
                foreach (var property in type.GetProperties())
                {
                    var getMethod = property.GetGetMethod();
                    var setMethod = property.GetSetMethod();

                    if (!IgnoreDataMember(property) && 
                        
                        property.CanRead &&
                        
                        getMethod != null &&
                        !getMethod.IsStatic &&
                        getMethod.IsPublic &&

                        ((property.CanWrite &&
                        setMethod != null &&
                        !setMethod.IsStatic &&
                        setMethod.IsPublic) || IsListProperty(property)) &&
                        
                        property.GetIndexParameters().Length == 0)
                    {
                        members.Add(property);
                    }
                }
                TypeProperties[type] = members;
                return members;
            }
            return TypeProperties[type];
        }

        private static bool IsListProperty(PropertyInfo property)
        {
            var typeInfo = property.PropertyType;
            if (typeInfo.GetInterfaces().Contains(typeof(IList)))
                return true;
            return false;
        }

        private static List<FieldInfo> GetFields(Type type)
        {
            if (!TypeFields.ContainsKey(type))
            {
                var members = new List<FieldInfo>();
                foreach (var field in type.GetFields())
                {
                    if (!field.IsStatic &&
                        field.IsPublic &&
                        !IgnoreDataMember(field))
                    {
                        members.Add(field);
                    }
                }
                TypeFields[type] = members;
                return members;
            }
            return TypeFields[type];
        }

        private static bool IgnoreDataMember(MemberInfo memberInfo)
        {
            return memberInfo.GetCustomAttributes(typeof(IgnoreDataMemberAttribute), true).FirstOrDefault() != null;
        }

        public static object Deserialize(Stream stream)
        {
            var typeProperties = new Dictionary<Type, List<PropertyInfo>>();
            var typeFields = new Dictionary<Type, List<FieldInfo>>();
            var types = new List<Type>();
            var objects = new Dictionary<int, object>();

            var headerPosition = ReadInt64(stream);
            stream.Position = headerPosition;

            ReadTypes(stream, types, typeFields, typeProperties);

            stream.Position = 0;
            ReadInt64(stream);

            return ReadObject(stream, objects, types, typeFields, typeProperties);
        }

        private static object ReadObject(Stream stream, Dictionary<int, object> objects, List<Type> types, Dictionary<Type, List<FieldInfo>> typeFields, Dictionary<Type, List<PropertyInfo>> typeProperties)
        {
            object obj = null;
            var objectTypeId = ReadInt32(stream);
            if (objectTypeId != -1)
            {
                var type = types[objectTypeId];

                if (!ReadPrimitiveValue(stream, ref obj, type))
                {
                    var typeInfo = type;
                    if (typeInfo.IsEnum)
                    {
                        var enumBaseType = Enum.GetUnderlyingType(type);
                        ReadPrimitiveValue(stream, ref obj, enumBaseType);
                    }
                    else
                    {
                        var isReference = ReadBool(stream);
                        var objectId = ReadInt32(stream);
                        if (isReference)
                        {
                            if (objects.ContainsKey(objectId))
                            {
                                obj = objects[objectId];
                            }
                            else
                            { 
                            
                            }
                        }
                        else 
                        {
                            if (typeInfo.IsArray)
                            {
                                obj = ReadArray(stream, type, types, objects, typeFields, typeProperties, objectId);
                            }
                            else if (typeInfo.GetInterfaces().Contains(typeof(IDictionary)))
                            {
                                obj = ReadDictionary(stream, type, types, objects, typeFields, typeProperties, objectId);
                            }
                            else if (typeInfo.GetInterfaces().Contains(typeof(IList)))
                            {
                                obj = ReadList(stream, type, types, objects, typeFields, typeProperties, objectId);
                            }
                            else
                            {
                                obj = ReadProperties(stream, type, types, objects, typeFields, typeProperties, objectId);
                            }
                        }
                    }
                }

            }

            return obj;
        }

        private static object ReadProperties(Stream stream, Type type, List<Type> types, Dictionary<int, object> objects, Dictionary<Type, List<FieldInfo>> typeFields, Dictionary<Type, List<PropertyInfo>> typeProperties, int objectId)
        {
            try
            {
                var obj = Activator.CreateInstance(type);
                objects[objectId] = obj;

                var fields = typeFields[obj.GetType()];
                foreach (var member in fields)
                {
                    var value = ReadObject(stream, objects, types, typeFields, typeProperties);
                    member.SetValue(obj, value);
                }

                var properties = typeProperties[obj.GetType()];
                foreach (var member in properties)
                {
                    if (IsListProperty(member) && !member.CanWrite && member.CanRead)
                    {
                        var baseList = member.GetValue(obj, null) as IList;
                        if (baseList != null)
                        {
                            var newList = ReadObject(stream, objects, types, typeFields, typeProperties) as IList;
                            if (newList != null)
                            {
                                foreach (var item in newList)
                                {
                                    baseList.Add(item);
                                }
                            }
                        }
                    }
                    else if (member.CanWrite)
                    {
                        var value = ReadObject(stream, objects, types, typeFields, typeProperties);
                        member.SetValue(obj, value, null);
                    }
                }

                return obj;
            }
            catch { }
            return null;
        }

        private static IList ReadList(Stream stream, Type type, List<Type> types, Dictionary<int, object> objects, Dictionary<Type, List<FieldInfo>> typeFields, Dictionary<Type, List<PropertyInfo>> typeProperties, int objectId)
        {
            var obj = Activator.CreateInstance(type) as IList;
            objects[objectId] = obj;
            var count = ReadInt32(stream);
            for (int i = 0; i < count; i++)
            {
                var value = ReadObject(stream, objects, types, typeFields, typeProperties);
                obj.Add(value);
            }
            return obj;
        }

        private static IDictionary ReadDictionary(Stream stream, Type type, List<Type> types, Dictionary<int, object> objects, Dictionary<Type, List<FieldInfo>> typeFields, Dictionary<Type, List<PropertyInfo>> typeProperties, int objectId)
        {
            var obj = Activator.CreateInstance(type) as IDictionary;
            objects[objectId] = obj;
            var count = ReadInt32(stream);
            for (int i = 0; i < count; i++)
            {
                var key = ReadObject(stream, objects, types, typeFields, typeProperties);
                var value = ReadObject(stream, objects, types, typeFields, typeProperties);
                obj.Add(key, value);
            }
            return obj;
        }

        private static object ReadArray(Stream stream, Type type, List<Type> types, Dictionary<int, object> objects, Dictionary<Type, List<FieldInfo>> typeFields, Dictionary<Type, List<PropertyInfo>> typeProperties, int objectId)
        {
            var rank = ReadInt32(stream);
            var lowerBound = ReadInt32(stream);
            var upperBound = ReadInt32(stream);
            var length = upperBound - lowerBound;
            var obj = Activator.CreateInstance(type, length) as Array;
            objects[objectId] = obj;
            for (int i = lowerBound; i < upperBound; i++)
            {
                var value = ReadObject(stream, objects, types, typeFields, typeProperties);
                obj.SetValue(value, i);
            }
            return obj;
        }

        private static bool ReadPrimitiveValue(Stream stream, ref object obj, Type type)
        {
            if (type == typeof(bool))
            {
                obj = ReadBool(stream);
                return true;
            }
            else if (type == typeof(char))
            {
                obj = ReadChar(stream);
                return true;
            }
            else if (type == typeof(byte))
            {
                obj = ReadByte(stream);
                return true;
            }
            else if (type == typeof(decimal))
            {
                obj = ReadDecimal(stream);
                return true;
            }
            else if (type == typeof(double))
            {
                obj = ReadDouble(stream);
                return true;
            }
            else if (type == typeof(float))
            {
                obj = ReadSingle(stream);
                return true;
            }
            else if (type == typeof(short))
            {
                obj = ReadInt16(stream);
                return true;
            }
            else if (type == typeof(int))
            {
                obj = ReadInt32(stream);
                return true;
            }
            else if (type == typeof(long))
            {
                obj = ReadInt64(stream);
                return true;
            }
            else if (type == typeof(string))
            {
                obj = ReadString(stream);
                return true;
            }
            else if (type == typeof(DateTime))
            {
                obj = ReadDateTime(stream);
                return true;
            }
            else if (type == typeof(TimeSpan))
            {
                obj = ReadTimeSpan(stream);
                return true;
            }
            else if (type == typeof(Guid))
            {
                obj = ReadGuid(stream);
                return true;
            }
            else if (type == typeof(ushort))
            {
                obj = ReadUInt16(stream);
                return true;
            }
            else if (type == typeof(uint))
            {
                obj = ReadUInt32(stream);
                return true;
            }
            else if (type == typeof(ulong))
            {
                obj = ReadUInt64(stream);
                return true;
            }   
            else if (type.FullName.Equals("System.RuntimeType"))
            {
                obj = Type.GetType(ReadString(stream));
                return true;
            }
            return false;
        }

        private static void ReadTypes(Stream stream, List<Type> types, Dictionary<Type, List<FieldInfo>> typeFields, Dictionary<Type, List<PropertyInfo>> typeProperties)
        {
            var typesCount = ReadInt32(stream);
            for (int i = 0; i < typesCount; i++)
            {
                var typeName = ReadString(stream);
                var type = Type.GetType(typeName);
                types.Add(type);

                var fields = new List<FieldInfo>();
                var fieldsCount = ReadInt32(stream);
                for (int j = 0; j < fieldsCount; j++)
                {
                    var fieldName = ReadString(stream);
                    fields.Add(type.GetField(fieldName));
                }
                typeFields[type] = fields;

                var properties = new List<PropertyInfo>();
                var propertiesCount = ReadInt32(stream);
                for (int j = 0; j < propertiesCount; j++)
                {
                    var propertyName = ReadString(stream);
                    properties.Add(type.GetProperty(propertyName));
                }
                typeProperties[type] = properties;
            }
        }
    }
}
